package ua.org.extjsbuilder.parser.scanner;

import java.util.LinkedList;
import java.util.List;

import ua.org.extjsbuilder.parser.IToken;
import ua.org.extjsbuilder.parser.ITokenScanner;
import ua.org.extjsbuilder.parser.JSParseException;
import ua.org.extjsbuilder.parser.runtime.CommentToken;
import ua.org.extjsbuilder.parser.runtime.NumberLiteralToken;
import ua.org.extjsbuilder.parser.runtime.RegexpLiteralToken;
import ua.org.extjsbuilder.parser.runtime.StringLiteralToken;
import ua.org.extjsbuilder.parser.runtime.UnparsedToken;

public class CommentsAndLiteralsScanner implements ITokenScanner {
	private static final int UNPARSED_STATE = 0;
	//Actualy the state below may be also regex literal
	private static final int OPEN_COMMENT_STATE = 1;
	private static final int MULTILINE_COMMENT_STATE = 2;
	private static final int SINGLE_LINE_COMMENT_STATE = 3;
	private static final int STRING_LITERAL_STATE = 4;
	private static final int NUMBER_LITERAL_START_STATE = 5;
	private static final int NUMBER_LITERAL_STATE = 6;
	private static final int REGEXP_LITERAL_STATE = 7;
	private static final int REGEXP_LITERAL_END_STATE = 8;
	
	private static final CharPattern BEFORE_NUMBER_LITERAL = CharPattern.compile("$.")
		.or(CharPattern.ALLOW_LETTERS_ONLY).or(CharPattern.ALLOW_DIGITS_ONLY).not();
	private static final CharPattern NUMBER_LITERAL_START = CharPattern.compile("-.")
		.or(CharPattern.ALLOW_DIGITS_ONLY);
	
	private static final CharPattern NUMBER_LITERAL = CharPattern.compile(".eExXaAbBcCdDeEfF")
		.or(CharPattern.ALLOW_DIGITS_ONLY);

	private List<IToken> result;
	private char[] data;
	private int state;
	private int start;
	private int i;
	private UnparsedToken token;

	public void init() {
		state = 0;
	}

	public synchronized void scan(IToken tok) {
		if(tok instanceof UnparsedToken){
			token = (UnparsedToken)tok;
			result = new LinkedList<IToken>();
			start = 0;
			data = token.getData();
			doScan();
		}
	}
	
	private void doScan(){
		i=0;
		for(;i<data.length;i++){
			analyseChar();
		}
		handleFinalState();
		token.split(result.toArray(new IToken[result.size()]));
	}
	
	public void finish() {
		if(state==MULTILINE_COMMENT_STATE){
			throw new JSParseException("Non-terminated comment",token.getLine());
		}
	}

	private void handleFinalState() {
		if(state==UNPARSED_STATE&&(start<i||(start==0&&i==0))){
			result.add((UnparsedToken)token.subSequence(start,i));
		}
		else if(state==OPEN_COMMENT_STATE){
			throw new JSParseException("Non-terminated regexp literal",token.getLine());
		}
		else if(state==SINGLE_LINE_COMMENT_STATE){
			result.add(new CommentToken(
				(UnparsedToken)token.subSequence(start,i)));
		}
		else if(state==MULTILINE_COMMENT_STATE){
			result.add(new CommentToken(
					(UnparsedToken)token.subSequence(start,i)));
			return;
		}
		else if(state==STRING_LITERAL_STATE){
			throw new JSParseException("Non-terminated string literal",token.getLine());
		}
		else if(state==NUMBER_LITERAL_START_STATE){
			UnparsedToken subToken = (UnparsedToken)token.subSequence(start,i);
			if(CharPattern.ALLOW_DIGITS_ONLY.match(data[start])){
				result.add(new NumberLiteralToken(subToken));
			}
			else {
				result.add(subToken);
			}
		}
		else if(state==NUMBER_LITERAL_STATE){
			UnparsedToken subToken = (UnparsedToken)token.subSequence(start,i);
			if(NumberLiteralToken.hasCorrectFormat(subToken)){
				result.add(new NumberLiteralToken(subToken));
			}
			else {
				result.add(subToken);
			}
		}
		else if(state==REGEXP_LITERAL_STATE){
			result.add((UnparsedToken)token.subSequence(start,i));
		}
		else if(state==REGEXP_LITERAL_END_STATE){
			result.add(new RegexpLiteralToken(
				(UnparsedToken)token.subSequence(start,i)));
		}
		state = UNPARSED_STATE;
	}

	private void analyseChar() {
		switch(state){
		case UNPARSED_STATE:
			handleUnparsed();
			return;
		case OPEN_COMMENT_STATE:
			state = getCommentOrRegexState(data[i]);
			return;
		case MULTILINE_COMMENT_STATE:
			handleMultilineComment();
			return;
		case SINGLE_LINE_COMMENT_STATE:
			handleSingleLineComment();
			return;
		case STRING_LITERAL_STATE:
			handleStringLiteral();
			return;
		case NUMBER_LITERAL_START_STATE:
			handleNumberStart();
			return;
		case NUMBER_LITERAL_STATE:
			handleNumberLiteral();
			return;
		case REGEXP_LITERAL_STATE:
			handleRegexpLiteral();
			return;
		case REGEXP_LITERAL_END_STATE:
			handleRegexpOptions();
			return;
		default:
			throw new IllegalStateException("Wrong state:"+state);	
		}
	}

	private void handleRegexpOptions() {
		if(CharPattern.ALLOW_LETTERS_ONLY.match(data[i])){
			return;
		}
		result.add(new RegexpLiteralToken(
			(UnparsedToken)token.subSequence(start,i+1)));
		state = UNPARSED_STATE;
		start = i+1;
	}

	private void handleRegexpLiteral() {
		if(data[i]=='/'&&!isEscaped(i-1)){
			state = REGEXP_LITERAL_END_STATE;
		}
	}

	private void handleNumberLiteral() {
		if(NUMBER_LITERAL.match(data[i])){
			return;
		}
		state = UNPARSED_STATE;
		UnparsedToken subToken = (UnparsedToken)token.subSequence(start,i);
		if(CharPattern.ALLOW_LETTERS_ONLY.match(data[i])){
			throw new JSParseException(
					"Javascript indefier cannot start with number", 
					token.getLine());
		}
		if(NumberLiteralToken.hasCorrectFormat(subToken)){
			result.add(new NumberLiteralToken(subToken));
		}
		else {
			result.add(subToken);
		}
		start = i;
	}

	private void handleNumberStart() {
		if(NUMBER_LITERAL.match(data[i])){
			state = NUMBER_LITERAL_STATE;
			return;
		}
		if(CharPattern.ALLOW_DIGITS_ONLY.match(data[start])){
			result.add(new NumberLiteralToken(
				(UnparsedToken)token.subSequence(start,i)));
		}
		else if(start<i){
			result.add(new UnparsedToken(data, start, i, token.getLine()));
		}
		state = UNPARSED_STATE;
		start = i;
	}

	private void handleStringLiteral() {
		if(data[start]==data[i]&&!isEscaped(i-1)){
			state = UNPARSED_STATE;
			result.add(new StringLiteralToken(
				(UnparsedToken)token.subSequence(start,i+1)));
			start = i+1;
		}
	}

	private void handleSingleLineComment() {
		if(i==(data.length-1)){
			state = UNPARSED_STATE;
			result.add(new CommentToken(
				(UnparsedToken)token.subSequence(start,i+1)));
			start = i+1;
		}
	}

	private void handleMultilineComment() {
		if(!isMultiCommentEnd())return;
		state = UNPARSED_STATE;
		i++;
		result.add(new CommentToken(
			(UnparsedToken)token.subSequence(start,i+1)));
		start = i+1;
	}

	private void handleUnparsed() {
		state = getNextState();
		if(state!=UNPARSED_STATE&&start!=i){
			result.add((UnparsedToken)token.subSequence(start,i));
			start = i;
		}
	}


	private boolean isEscaped(int i) {
		boolean result = false;
		while(i>=0&&data[i]=='\\'){
			i--;
			result=!result;
		}
		return result;
	}

	private boolean isMultiCommentEnd() {
		if((i+1)>=data.length){
			return false;
		}
		return data[i]=='*'&&data[i+1]=='/';
	}

	private int getCommentOrRegexState(char c) {
		if(c=='/')return SINGLE_LINE_COMMENT_STATE;
		if(c=='*')return MULTILINE_COMMENT_STATE;
		return REGEXP_LITERAL_STATE;
	}

	private int getNextState() {
		char c = data[i];
		if(c=='/'){
			return OPEN_COMMENT_STATE;
		}
		if(c=='\''||c=='"'){
			return STRING_LITERAL_STATE;
		}
		if((i==0||BEFORE_NUMBER_LITERAL.match(data[i-1]))&&
				NUMBER_LITERAL_START.match(c)){
			return NUMBER_LITERAL_START_STATE;
		}
		return UNPARSED_STATE;
	}
}
